package com.schuxl.htmlobserver.observer;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.mail.internet.InternetAddress;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;

import org.apache.log4j.Logger;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;

import util.DistanceCalculator;
import util.Util;

import com.schuxl.htmlobserver.StartHtmlObserver;
import com.schuxl.htmlobserver.dataobject.Ad;
import com.schuxl.htmlobserver.dataobject.WillhabenAd;
import com.schuxl.htmlobserver.dataobject.schemaTypen.filter.Bike;
import com.schuxl.htmlobserver.dataobject.schemaTypen.filter.Bikes;
import com.schuxl.htmlobserver.dataobject.schemaTypen.filter.Model;
import com.schuxl.htmlobserver.dataobject.schemaTypen.sites.Sites.Site;
import com.schuxl.htmlobserver.mail.MailSender;


public abstract class HtmlObserver<T extends Ad> implements Runnable {

	
	private static Logger LOGGER = Logger.getLogger(HtmlObserver.class);
	private static Bikes BIKES = null;
	protected Site site;
	public static final DateFormat DATE_FORMATTER = new SimpleDateFormat("dd.MM.yy 'um' HH:mm:ss");
	
	
	
	static {
		try {
			JAXBContext jaxbContext = JAXBContext.newInstance("com.schuxl.htmlobserver.dataobject.schemaTypen.filter");
			Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
		    BIKES = (Bikes)unmarshaller.unmarshal(new File(StartHtmlObserver.PROPERTIES.getProperty("com.schuxl.htmlobserver.modelFilterFile")));
		} catch(Exception e) {
			LOGGER.error("Fehler in Klasse HtmlObserver beim Instanzieren des JAXB-Contexts!");
		}
		
	}
	
	
	public HtmlObserver(Site site) {
		this.site=site;
	}
	
	
	/**
	 * Erstellt aus dem uebergebenen Document eine Liste von Objekten. 
	 * @param document Document Object einer Site
	 * @return Die erstellte Objektliste
	 */
	protected abstract List<T> getCurrentList(Document document);
	

	
	
	
	@Override
	public synchronized void run() {
		while(true) {
			try {
				LOGGER.debug("Holen der Daten von " + site.getUrl());
				URL url = new URL(site.getUrl());
				
				LOGGER.debug("Parsen der Daten von " + site.getUrl());
				Document document = Jsoup.parse(url, 1000*20);
				List<T> currentList = getCurrentList(document);
				
				String tempFileName = StartHtmlObserver.PROPERTIES.getProperty("com.schuxl.htmlobserver.tempFileFolder")+site.getId()+".ser";
	            LOGGER.debug("Lese fileList aus dem File " + tempFileName);
	            File tempFile = new File(tempFileName);
	            List<T> deltaList = new ArrayList<T>();
	            List<T> fileList = null;
	            //Falls es noch keine fileList gibt, wird alles als Aenderung betrachtet und versendet
	    		if(tempFile.exists()==false) {
	    			deltaList = currentList;
	    		} else { //Aktuelle und letzte List vergleichen und die Aenderung senden
	    			LOGGER.debug("Vergleiche Daten von currentList und fileList");
	    			fileList = readFile(tempFileName);
	    		    for(T currentAd : currentList) {
	    		    	boolean foundIt = false;
	    		    	for(T fileAd: fileList) {
	    		    		if(fileAd.equals(currentAd)) {
	    		    			foundIt = true;
	    		    			break;
	    		    		} 
	    		    	}
	    		    	if(foundIt==false)
	    		    		deltaList.add(currentAd);
	    		    }
	    		}
	    		List<T> filteredList = filterRelevantBikes(deltaList);
	    		List<T> populatedList = calculateSupplemantaryParameters(filteredList);
	    		buildAndSendMail(populatedList);
	    		
	    		LOGGER.debug("Schreibe Listen in File " + tempFileName);
	    		List<T> list = mergeAdList(currentList, fileList);
	    		list = cleanupAdList(list);
	    		writeFile(tempFileName, list);
	    	    
		        LOGGER.debug("Thread wird fuer "+site.getIntervallInMinutes()+" Minuten in Schlaf versetzt");
				Thread.sleep(Integer.parseInt(site.getIntervallInMinutes())*60*1000);
			}
			catch(Exception e) {
				LOGGER.error("Exception: ", e);
				try {
					Thread.sleep(5*60*1000); //Sleep 5 minutes
				} catch (InterruptedException e1) {
					LOGGER.error(e);
				} 
			}
		}
	}
	
	

	


	/**
	 * Gibt alle neuen Ads zu den bisherigen Ads dazu.
	 * @param currentList
	 * @param fileList
	 * @return mergedList
	 */
	private synchronized List<T> mergeAdList(List<T> currentList, List<T> fileList) {
		//Wenn es noch keine fileList gab ist sie NULL und daher ist die currentList die gemergte Liste
		if(fileList==null)
			return currentList;
		
		//Mergen der Listen
		List<T> mergedList = new ArrayList<T>();
		mergedList.addAll(fileList); //Die von der aktuellen Liste kommen schon mal auf jeden Fall rein
		for(T currentAd : currentList) {
	    	boolean foundIt = false;
	    	for(T fileAd: fileList) {
	    		if(fileAd.equals(currentAd)) {
	    			foundIt = true;
	    			break;
	    		} 
	    	}
	    	if(foundIt==false)
	    		mergedList.add(currentAd);
	    }
		return mergedList;
	}
	
	
	/**
	 * Ads die schon aelter als 1 Woche sind, werden ausgemistet
	 * @param list
	 * @return Die bereinigte Liste
	 */
	private synchronized List<T> cleanupAdList(List<T> list) {
		Calendar datumVorSiebenTagen = Calendar.getInstance();
		datumVorSiebenTagen.add(Calendar.DATE, -7);
		
		//Vergabe der Zeitstempel (IssueDate) und ausmisten der alten Ads
		//Dies muss mit einem Iterator geloest werden (nicht foreach), da man aus Listen ueber die man schleift, keine Elemente loeschen kann
		Iterator<T> iterator = list.iterator();
		while (iterator.hasNext()) {
		    T currentAd = iterator.next();
		    
		    //Alle die noch keinen Zeitstempel (IssueDate) haben, bekommen jetzt einen)
			if(currentAd.getIssueDate()==null)
				currentAd.setIssueDate(Calendar.getInstance());
			//All diejenigen deren IssudeDate aelter als 7 Tage ist fallen raus
			if(currentAd.getIssueDate().before(datumVorSiebenTagen))
				iterator.remove();
		}
		return list;
	}


	/**
	 * Filtered aus der deltaList die dem model-filter entsprechenden Bikes heraus.
	 * @param deltaList
	 * @return Dem Filter entsprechende Bikes
	 */
	private synchronized List<T> filterRelevantBikes(List<T> deltaList) {
		List<T> filteredList = new ArrayList<T>();
		//Mit der FOREACH Schleife hat er die Schleife leider scheinbar 2x gemacht
		currentAd:
		for(int i=0; i<deltaList.size(); i++) {
			T currentAd = deltaList.get(i);
			
			//Ausfiltern von ausl. Fahrzeugen mit zu grossem Hubraum (Die Nova ist hier ohnehin zu teuer)
			if("Y".equals(site.getForeign()) && currentAd.getCcm()!=null && currentAd.getCcm()>850)
				continue currentAd;
			
			for(Bike bike : BIKES.getBike()) {
				
				//Finde die weissen Schafe
				Pattern filterPattern = Pattern.compile(bike.getFilter(), Pattern.CASE_INSENSITIVE);
		        Matcher filterMatcher = filterPattern.matcher(currentAd.getTitle());
		        //Verbanne die schwarzen Schafe
		        boolean isBlackSheep = false;
		        if(bike.getBlackSheep()!=null) {
		        	Pattern blackSheepPattern = Pattern.compile(bike.getBlackSheep(), Pattern.CASE_INSENSITIVE);
			        if(blackSheepPattern.matcher(currentAd.getTitle()).find()==true)
			        	isBlackSheep = true;
		        }
		        
		        
		        if(filterMatcher.find()==true && isBlackSheep==false) {
					for(Model model : bike.getModels().getModel()) {
						int price = 0;
						
						//Abzuege fuer ausl. Fahrzeuge
						String priceCalculation = "Preisgrenze: "+Util.formatPrice(model.getPrice())+",-";
						if("N".equals(site.getForeign())) {
							price = model.getPrice();
						}else {
							price = model.getPrice()/100*80;
							priceCalculation+=", Abzug(20%) Ausland = "+Util.formatPrice(price)+",-";
						}

						//Abzuege fuer erhoehte Kilometerleistung
						int km = Util.getKilometersFromString(currentAd.getKilometers());
						if(km >= 40000) {
							price = price/100*60;
							priceCalculation+=", Abzug(40%) +40tkm = "+Util.formatPrice(price)+",-";
						}else if(km >= 30000) {
							price = price/100*75;
							priceCalculation+=", Abzug(25%) +30tkm = "+Util.formatPrice(price)+",-";
						}else if(km >= 20000) {
							price = price/100*85;
							priceCalculation+=", Abzug(15%) +20tkm = "+Util.formatPrice(price)+",-";
						}else if(km >= 15000) {
							price = price/100*90;
							priceCalculation+=", Abzug(10%) +15tkm = "+Util.formatPrice(price)+",-";
						}else if(km < 0) {
							priceCalculation+=", Kilometerleistung unbekannt!";
						}
						
						if(currentAd.getYear().equals(model.getYear()) && currentAd.getPrice() <= price && currentAd.getPrice() > 350) { //die 350 weil oft nur Teile statt ganzen Motorrädern angeboten werden und manche zuerst mit 0€ einstellen.
							currentAd.setBikeName(bike.getTitle()); //Um zu wissen welcher Filter gezogen hat
							currentAd.setPriceCalculation(priceCalculation);
							filteredList.add(currentAd);
							continue currentAd; //Damit ein allgemeiner Filter (z.B. GSX-R bei GSX-R 600) nicht nochmal zieht und added
						}
					}
				}
			}
	    }
		return filteredList;
	}
	
	
	
	
	/**
	 * Ergaenzt die Ads um zusaetzliche Parameter
	 * @param filteredList
	 * @return Die Liste mit den erweiterten Ads
	 */
	private List<T> calculateSupplemantaryParameters(List<T> filteredList) {
//		List<T> detailSiteParameterList = fetchDetailSiteParameters(filteredList);
		List<T> travelParameterList = calculateTravelParameters(filteredList);
		return travelParameterList;
	}
	
	
	//Wird momentan nicht benoetigt, da Willhaben die Location wieder auf der Uebersichtsseite anbietet
	/**
	 * Zumindest fuer willhaben.at Sites wird die Location aus der Detailseite nachgeladen 
	 * @param filteredList
	 * @return Die Liste der Ads mit der detaillierten Location
	 */
//	private List<T> fetchDetailSiteParameters(List<T> filteredList) {
//		for(Ad ad : filteredList) {
//			if(ad instanceof WillhabenAd) {
//				LOGGER.debug("Hole Daten aus Detailseite: "+ad.getLink());
//				try {
//					Document tbodyDocument = Jsoup.parse(new URL(ad.getLink()), 1000*15);
//					Element locationElement = tbodyDocument.select("div.block.right div.contentboxBody div.contentboxBodyBlock table tbody > tr:contains(Adresse) td").first();
//					ad.setLocation(locationElement.text());
//				} catch (Exception e) {
//					LOGGER.error("Fehler bei Holen der Location aus der Detailseite." + e);
//				}
//			}
//	    }
//		return filteredList;
//	}
	
	


	/**
	 * Holt fuer jedes Bike die Entfernung von Wien/Dobersberg bis dorthin wo das Bike steht und befuellt die entsprechenenden Properties
	 * Wenn der Ort nicht eindeutig ist oder nicht erkannt wird, dann wird es leer gelassen
	 * @param filteredList
	 * @return Gibt die erweiterte/befuellte Liste zurueck
	 */
	private List<T> calculateTravelParameters(List<T> filteredList) {
		for(int i=0; i<filteredList.size(); i++) {
			Ad ad = filteredList.get(i);
			Map<String,String> mapWien = DistanceCalculator.addDistanceAndDuration("Wien", ad.getLocation());
			ad.setDistanceWien(mapWien.get(DistanceCalculator.MAP_DISTANCE));
			ad.setDurationWien(mapWien.get(DistanceCalculator.MAP_DURATION));
			ad.setLinkWien(mapWien.get(DistanceCalculator.MAP_LINK));
			Map<String,String> mapDobersberg = DistanceCalculator.addDistanceAndDuration("Dobersberg", ad.getLocation());
			ad.setDistanceDobersberg(mapDobersberg.get(DistanceCalculator.MAP_DISTANCE));
			ad.setDurationDobersberg(mapDobersberg.get(DistanceCalculator.MAP_DURATION));
			ad.setLinkDobersberg(mapDobersberg.get(DistanceCalculator.MAP_LINK));
	    }
		return filteredList;
	}

	
	
	
	/**
	 * Baut den Mailinhalt zusammen u. versendet das Mail
	 * @param List
	 */
	private synchronized void buildAndSendMail(List<T> filteredList) throws Exception {
		LOGGER.debug("Mail Inhalt zusammenbauen.");
		StringBuffer stringBuffer = new StringBuffer();
		stringBuffer.append("<html><head/><body>");
		stringBuffer.append("<hr/>");
		for(T ad : filteredList) {
			stringBuffer.append(ad.getHtmlRepresentation());
			stringBuffer.append("<hr/>");
		}
		stringBuffer.append("</body></html>");
		
		if(filteredList.size()>0) {
			LOGGER.debug("Mail wird an Subscriber gesendet.");
			InternetAddress[] empfaengerListe = new InternetAddress[site.getSubscribers().getSubscriber().size()];
			for(int i=0; i<site.getSubscribers().getSubscriber().size();i++) {
				InternetAddress internetAddress = new InternetAddress(site.getSubscribers().getSubscriber().get(i).getEmail());
				empfaengerListe[i]=internetAddress;
			}
			MailSender.sendMessage(empfaengerListe, site.getId()+" - "+DATE_FORMATTER.format(new Date()), stringBuffer.toString());
		}
	}



	/**
	 * Liest das File ein, dass die Daten der letzten Abfrage enthaelt
	 * @param fileName
	 * @return
	 * @throws Exception
	 */
	protected List<T> readFile(String fileName) throws Exception {
		ObjectInputStream ois = null;
		try {
		    FileInputStream fis = new FileInputStream(fileName);
		    ois = new ObjectInputStream(fis);
		    return (List<T>) ois.readObject();
		} finally {
			ois.close();
		}
	}
	
	
	
	/**
	 * Schreibt das File weg, dass die Daten der aktuellen Abfrage enthaelt
	 * @param fileName
	 * @param currentList
	 * @throws Exception
	 */
	protected void writeFile(String fileName, List<T> currentList) throws Exception {
		ObjectOutputStream oos = null;
		try {
		    FileOutputStream fos = new FileOutputStream(fileName);
		    oos = new ObjectOutputStream(fos);
		    oos.writeObject(currentList);
		} finally {
			oos.flush();
			oos.close();
		}
	}
	
	
	

	
	
	
}
